#pragma once

#include "Common.hpp"
#include "SocketTemplate.hpp"
#include "Log.hpp"
#include <sys/select.h>

#define NUM sizeof(fd_set) * 8

const int gdefaultfd = -1;

using namespace SocketModule;
using namespace LogModule;

class SelectServer
{
public:
    SelectServer(uint16_t port)
        :_port(port)
        ,_listen_socket(std::make_unique<TcpSocket>())
        ,_isrunning(false)
    {}

    void Init()
    {
        _listen_socket->BuildTcpSocketMethod(_port);  // 创建套接字
        // 初始化辅助数组
        for(int i = 0;i < NUM;i++)
            _fd_array[i] = gdefaultfd;
        // 添加listensockfd到辅助数组中
        _fd_array[0] = _listen_socket->Fd();
    }

    void Loop()
    {
        // 创建完listensockfd后，如果使用accept获取新链接，而accept本质不是阻塞的吗？
        // 所以说从listensockfd中获取新链接，本质也是一种IO，而这种IO特殊在不是传送数据的IO，这种IO只做input，也就是说listensockfd只关心读事件是否就绪！读就绪了，代表新连接就绪了
        // 而我们要实现的select多路转接，就不能使用阻塞的方式，所以要将listensockfd添加到select内部，让select来帮我们等（管理读事件是否就绪）
        // _listen_socket->Accepter(client)
        // 所以第一步就要创建一个读文件描述符集

        fd_set rfds;  // 读文件描述符集
        _isrunning = true;
        while(_isrunning)
        {
            // 由于rfds是一个输入输出型参数，而且在while循环中互相影响，所以第一步就要
            // 清空rfds
            FD_ZERO(&rfds);
            
            struct timeval timeout = {5,0};  // 更新timeout
            int maxfd = gdefaultfd;  // 用于更新select的第一个参数 nfds，是最大的文件描述符+1
            for(int i = 0;i < NUM;i++)
            {
                if(_fd_array[i] == gdefaultfd) continue;
                // 将合法参数设置进去
                FD_SET(_fd_array[i], &rfds);  // 包括listensockfd
                // 更新最大值
                maxfd = std::max(maxfd, _fd_array[i]);
            }
            // 将listensockfd添加到rfds中
            // FD_SET(_listen_socket->Fd(), &rfds);

            // 我们不能让accept来阻塞地获取新连接，而应该让select来负责进行就绪事件的检测
            // 用户告诉内核：你要帮我关心 &rfds 中的读事件  
            int n = select(maxfd+1, &rfds, nullptr, nullptr, &timeout);
            switch(n)
            {
                case 0:
                    // 超时机制：经过timeout时间后如果没有就绪的，就返回 0
                    cout << "time out..." << endl;
                    break;
                case -1:
                    // select 失败
                    perror("select failed");
                    break;
                default:
                    // 一旦有事件就绪了，就会有剩余timeout
                    // rfds：内核告诉用户，你关心的rfds中的fd，有哪些已经就绪了，你得处理它！
                    std::cout << "有事件就绪了..., timeout: " << timeout.tv_sec << '.' << timeout.tv_usec << std::endl;
                    // 把已经就绪的sockfd，派发给指定的模块
                    Dispatcher(rfds);
                    break;
            }
        }
        _isrunning = false;
    }

    // 如果是一个listensockfd，那就执行链接的操作
    void Accepter()
    {
        InetAddr client;
        // IO = 等 + 拷贝
        // 说明listensockfd已经就绪了，此时直接进行accept，不会被阻塞，相当于只会进行IO中的“拷贝”工作！
        int newfd = _listen_socket->Accepter(&client);
        if(newfd < 0) return;
        else
        {
            std::cout << "获得了一个新链接：" << newfd << " client info: " << client.Addr() << std::endl; 
            // 新获取的newfd，能立马recv吗？不能！因为不知道就没就绪，如果没就绪，就会阻塞住！所以只能先添加到辅助数组中，下一次再读取
            int pos = gdefaultfd;
            for(int j = 0;j < NUM;j++)
            {
                if(_fd_array[j] == gdefaultfd)  // 说明没有用过，可以存进去
                {
                    pos = j;
                    break;
                }
            }
            if(pos == gdefaultfd)  // 如果遍历一遍还没有结果，说明辅助数组中已经满了
            {
                LOG(LogLevel::ERROR) << "服务器已经满了....";
                close(newfd);
            }
            else   // 否则添加进去
                _fd_array[pos] = newfd;
        }
    }

    // 如果这是一个普通的fd，那就读取
    void Recver(int who)
    {
        char buffer[1024];
        // 此时的recv不会被阻塞，因为fd已经就绪了 
        // 这里的recv，是不完善的，因为不一定读到了所有的请求，想读完整的报文，必须得有协议，所以这里还是有bug的
        ssize_t n = ::recv(_fd_array[who], buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "client# " << buffer << std::endl;
            // 回显回去
            std::string message = "echo# ";
            message += buffer;
            ::send(_fd_array[who], message.c_str(), message.size(), 0);
        }
        else if(n == 0)  // 说明链接关了
        {
            LOG(LogLevel::DEBUG) << "客户端退出了，sockfd: " << _fd_array[who];
            // 关闭文件描述符
            close(_fd_array[who]);
            // 这个fd是不合法的了，所以要从辅助数组中移除
            _fd_array[who] = gdefaultfd;
        }
        else
        {
            LOG(LogLevel::DEBUG) << "客户端读取出错了，sockfd: " << _fd_array[who];
            // 关闭文件描述符
            close(_fd_array[who]);
            // 这个fd是不合法的了，所以要从辅助数组中移除
            _fd_array[who] = gdefaultfd;
        }
    }

    // 要处理的文件描述符，一定在rfds里
    void Dispatcher(fd_set& rfds)  // 如果有越来越多的fd进来了，就可能有更多的fd就绪，就不仅仅是listensockfd
    {
        for(int i = 0;i < NUM;i++)
        {
            // 如果这不是一个合法的fd，跳过
            if(_fd_array[i] == gdefaultfd) continue;
            // 这是一个合法的fd
            // 如果这是listensockfd，就是一个链接的动作
            if(_fd_array[i] == _listen_socket->Fd())
            {
                // 首先验证listensockfd是否在rfds里
                if(FD_ISSET(_fd_array[i], &rfds))
                {
                    Accepter();
                }
            }
            else
            {
                if(FD_ISSET(_fd_array[i], &rfds))
                {
                    // 这是一个合法的、普通的、就绪的fd！
                    // 读取第i个文件描述符
                    Recver(i);  
                }
            }
        }
    }

    ~SelectServer(){}

private:
    uint16_t _port;  // 端口号
    std::unique_ptr<Socket> _listen_socket;  // tcp套接字
    bool _isrunning;  // 程序是否运行
    int _fd_array[NUM];  // 辅助数组：用来更新文件描述符集
};